package com.ams.studio.xml;


import org.apache.commons.lang.StringUtils;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 * Created by zhongyl on 2015/12/4.
 */
public class XmlHelper {

    private Document document;

    public XmlHelper() {
    }

    public Document getDocument() {
        return document;
    }

    private void NewDocument(String rootNodeName) {
        document = DocumentHelper.createDocument();
        if (!StringUtils.isBlank(rootNodeName)) {
            createDocumentElement(rootNodeName);
        }
    }

    /**
     * 设置 xpath 指定的节点的内容。如果节点不存在，则先（递归地）创建节点
     *
     * @param xpath 节点路径，从根节点开始，以 / 开头
     * @param value @Nullable 节点的内容
     */
    public XmlNodeContext insertOrUpdateNode(String xpath, String value) {
        if (StringUtils.isBlank(xpath)) {
            throw new IllegalArgumentException("xpath");
        }

        if (!xpath.startsWith("/")) {
            throw new IllegalArgumentException("xpath must start with /");
        }

        Node node = createNodeRecursive(xpath);
        if (StringUtils.isBlank(value)) {
            node.setText("");
        } else {
            node.setText(value.trim());
        }
        return new XmlNodeContext(this, node);
    }

    /**
     * 设置 xpath 指定的节点的内容。如果节点不存在，则先（递归地）创建节点。xpath 为基于父节点的相对路径。
     *
     * @param xpath 节点路径，从根节点开始，以 / 开头
     * @param value @Nullable 节点的内容
     */
    public XmlNodeContext insertOrUpdateNode(XmlNodeContext parentNode, String xpath, String value) {
        if (parentNode == null) {
            return insertOrUpdateNode(xpath, value);
        }

        if (StringUtils.isBlank(xpath)) {
            throw new IllegalArgumentException("xpath");
        }

        Node node = createNodeRecursive((Element) parentNode.getNode(), xpath);
        if (StringUtils.isBlank(value)) {
            node.setText("");
        } else {
            node.setText(value.trim());
        }
        return new XmlNodeContext(parentNode.getHelper(), node);
    }

    /**
     * 设置 xpath 指定的节点的指定属性的值。如果节点不存在，则先（递归地）创建节点。如果属性不存在，则创建属性。
     *
     * @param xpath          节点路径，从根节点开始，以 / 开头
     * @param attributeName  属性名称
     * @param attributeValue @Nullable 节点的内容
     */
    public void insertOrUpdateNodeAttribute(String xpath, String attributeName, String attributeValue) {
        XmlNodeContext context = insertOrUpdateNode(xpath, null);
        Element node = (Element) context.getNode();
        setAttribute(node, attributeName, attributeValue);
    }

    /**
     * 设置 xpath 指定的节点的指定属性的值。如果节点不存在，则先（递归地）创建节点。如果属性不存在，则创建属性。
     * <p>
     * xpath 为基于父节点的相对路径。
     *
     * @param parentNode     父节点
     * @param xpath          节点路径，从根节点开始，以 / 开头
     * @param attributeName  属性名称
     * @param attributeValue @Nullable 节点的内容
     */
    public void insertOrUpdateNodeAttribute(XmlNodeContext parentNode, String xpath,
            String attributeName, String attributeValue) {
        XmlNodeContext context = insertOrUpdateNode(parentNode, xpath, null);
        Element node = (Element) context.getNode();
        setAttribute(node, attributeName, attributeValue);
    }

    private void setAttribute(Element node, String attributeName, String attributeValue) {
        Attribute attr = node.attribute(attributeName);
        if (attr == null) {
            attr = DocumentHelper.createAttribute(node, attributeName, attributeValue);
            node.add(attr);
        } else {
            attr.setValue(attributeValue);
        }
    }

    /**
     * 获取节点的内容，如果节点不存在，则返回 null
     *
     * @param xpath
     * @return
     */
    public String getNodeValue(String xpath) {
        Node node = document.selectSingleNode(xpath);
        if (node != null) {
            String text = node.getText();
            return text == null ? text : text.trim();
        } else {
            return null;
        }
    }

    public void deleteNode(String xpath) {
        Node node = document.selectSingleNode(xpath);
        if (node != null) {
            Element parent = node.getParent();
            if (parent != null) {
                parent.remove(node);
            } else {
                document.remove(node);
            }
        }
    }

    public boolean isNodeExist(String xpath) {
        return null != document.selectSingleNode(xpath);
    }

    public void load(String xml) throws DocumentException {
        load(xml, null);
    }

    public void load(String xml, String encoding) throws DocumentException {
        if (StringUtils.isBlank(xml)) {
            throw new IllegalArgumentException("xml");
        }
        if (!StringUtils.isBlank(encoding)) {
            document = DocumentHelper.parseText(processEncoding(xml, encoding));
        } else {
            document = DocumentHelper.parseText(xml);
        }
    }

    private String processEncoding(String xml, String encoding) {
        int start = xml.indexOf("<?xml");
        int end = xml.indexOf("?>");
        int cutStart = 0;
        if (start >= 0 && end >= 0) {
            int encodingIndex = xml.substring(start, end).indexOf("encoding=");
            if (encodingIndex > 0) {
                return xml;
            } else {
                cutStart = end + 2;
            }
        }
        StringBuilder sb = new StringBuilder(xml.length() + 64);
        sb.append("<?xml version=\"1.0\" encoding=\"");
        sb.append(encoding);
        sb.append("\"?>");
        sb.append(xml.substring(cutStart));
        return sb.toString();
    }

    public String export(String encoding) {
        if (document == null) {
            return null;
        }

        OutputFormat format = new OutputFormat(null, false, encoding);
        format.setExpandEmptyElements(true);
        format.setIndent(false);
        format.setTrimText(true);
        format.setSuppressDeclaration(false);

        return export(format);
    }

    public String export(OutputFormat format) {
        if (document == null) {
            return null;
        }
        ByteArrayOutputStream stream = new ByteArrayOutputStream(1024);
        XMLWriter xw = null;
        try {
            xw = new XMLWriter(stream, format);
            xw.write(document);
            xw.flush();
            xw.close();

            return stream.toString(format.getEncoding());
        } catch (IOException ex) {
            return null;
        } finally {
            try {
                xw.close();
            } catch (IOException e) {
            }
        }
    }

    /**
     * TO confirm throws error when document has root element already
     *
     * @param rootNodeName
     * @return
     */
    private Element createDocumentElement(String rootNodeName) {
        Element element = DocumentHelper.createElement(rootNodeName);
        document.setRootElement(element);
        return element;
    }

    private Element createNodeRecursive(String xpath) {
        return createNodeRecursive(null, xpath);
    }

    private Element createNodeRecursive(Element node, String xpath) {
        Element childNode = null;
        String[] segments = xpath.split("/");
        for (String nodeName : segments) {
            if (StringUtils.isBlank(nodeName))
                continue;

            if (node == null) {
                if (document == null) {
                    NewDocument(nodeName);
                }

                node = document.getRootElement();
                if (node == null) {
                    createDocumentElement(nodeName);
                } else if (!StringUtils.equals(node.getName(), nodeName)) {
                    throw new RuntimeException("root node not match");
                }

                childNode = node;
            } else {
                childNode = (Element) node.selectSingleNode(nodeName);
                if (childNode == null) {
                    childNode = DocumentHelper.createElement(nodeName);
                    node.add(childNode);
                }

                node = childNode;
            }
        }

        return childNode;
    }


    public XmlLoopContext getNodes(String xpath) {
        return new XmlLoopContext(this, document.selectNodes(xpath));
    }

    public XmlLoopContext getNodes(XmlLoopContext context, String subXPath) {
        return new XmlLoopContext(this, context.currentNode().selectNodes(subXPath));
    }

    public XmlLoopContext getChildNodes(String xpath) {
        return new XmlLoopContext(this, document.selectNodes(xpath + "/*"));
    }

    public XmlLoopContext getChildNodes(XmlLoopContext context, String subParentXPath) {
        return new XmlLoopContext(this, context.currentNode().selectNodes(subParentXPath + "/*"));
    }

    public XmlNodeContext getSingleNode(String xpath) {
        return new XmlNodeContext(this, document.selectSingleNode(xpath));
    }

    public String getAttributeValue(String xpath, String attributeName) {
        return getNodeValue(xpath + "/@" + attributeName);
    }

    /**
     * /a: 创建 a 作为 xml 的根节点，如果根节点已经存在，则报错。
     * <p>
     * /a/b/c: 先找到 /a/b 节点，如果没有则创建。然后不管 b 节点下是否有 c 节点，都创建 c 节点。
     * <p>
     * /a/b/c/// 等同于 /a/b/c.
     *
     * @param xpath
     * @param value
     * @return
     */
    public XmlNodeContext addNode(String xpath, String value) {
        int index = StringUtils.stripEnd(xpath, "/").lastIndexOf("/");
        String nodeName = xpath.substring(index + 1);
        String parentXPath = index > 0 ? xpath.substring(0, index) : null;
        if (StringUtils.isBlank(parentXPath)) {
            return insertOrUpdateNode(xpath, value);
        } else {
            XmlNodeContext context = insertOrUpdateNode(parentXPath, null);
            Element element = DocumentHelper.createElement(nodeName);
            if (StringUtils.isNotBlank(value)) {
                element.setText(value);
            }
            ((Element) context.getNode()).add(element);
            return new XmlNodeContext(this, element);
        }
    }

    /**
     * 如果 parentNode 为空，则等同于 addNode(xpath).
     * <p>
     * /a: 在 parentNode 的根节点下创建 a 子节点（不管是否已经存在同名节点）。
     * <p>
     * /a/b/c: 先在 parentNode 节点下找到 /a/b 子节点，如果没有则创建。然后不管 b 节点下是否有 c 节点，都创建 c 节点。
     * <p>
     * /a/b/c/// 等同于 /a/b/c 等同于 a/b/c.
     *
     * @param parentNode
     * @param xpath
     * @param value
     * @return
     */
    public XmlNodeContext addNode(XmlNodeContext parentNode, String xpath, String value) {
        if (parentNode == null) {
            return addNode(xpath, value);
        }
        int index = StringUtils.stripEnd(xpath, "/").lastIndexOf("/");
        String nodeName = xpath.substring(index + 1);
        String parentXPath = index > 0 ? xpath.substring(0, index) : null;
        if (StringUtils.isBlank(parentXPath)) {
            Element element = DocumentHelper.createElement(nodeName);
            ((Element) parentNode.getNode()).add(element);
            return new XmlNodeContext(parentNode.getHelper(), element);
        } else {
            XmlNodeContext context = insertOrUpdateNode(parentNode, parentXPath, null);
            Element element = DocumentHelper.createElement(nodeName);
            if (StringUtils.isNotBlank(value)) {
                element.setText(value);
            }
            ((Element) context.getNode()).add(element);
            return new XmlNodeContext(parentNode.getHelper(), element);
        }
    }
}
